feat: execution recovery safeguards - #97
Conversation
…98) * docs: KEEP-1132 add contribution guide and require an accepted issue This repo had no CONTRIBUTING.md. Adds one covering the real build (make build/test/lint, the sync-version step, `go generate ./docs/` which CI fails on drift) and ISSUES.md as the contribution policy. Anything that changes command behaviour needs an issue marked `accepted` before the pull request, referenced in the PR title after the conventional-commit type (`feat: #97 description`). The type prefix still drives release-please. Issue forms and pr-issue-link.yml match the keeperhub setup, retargeted to main since this repo has no staging branch. Needs before this is useful: the `accepted`, `needs-triage` and `no-issue-required` labels. * docs: KEEP-1132 require reason, scope and plan on every issue Mirrors the keeperhub change. Issue forms restructured around three required parts: reason (command, output, expectation and its source, what it costs), scope (commands affected, siblings checked, one problem), and plan (the filer's proposal, which triage may replace in a comment before accepting). Adds `confirmed` so reproducing an issue is separable from deciding to fix it. * docs: KEEP-1132 never bounce an issue for its format Mirrors the keeperhub change: nothing applies retroactively, a filer is never asked to restate what they already said, and compound issues are split by us with the reporter credited on each part.
suisuss
left a comment
There was a problem hiding this comment.
First PR here - CONTRIBUTING.md covers the conventions, and ISSUES.md covers when a change needs an accepted issue first.
What this changes
12 new files, +175/-0. docs/execution-recovery-v1/contract.md states six normative rules R1-R6 and a table mapping each of 10 JSON fixtures to the rule it exercises; the fixtures land under testdata/execution_recovery_v1/; README_EXECUTION_RECOVERY.md is a root-level pointer to both. No Go code, no test, no CI change - go build ./cmd/kh produces the same binary, and go test ./... never walks testdata/.
Does it match the description
Undersells. The summary names the fixtures and the rules but not README_EXECUTION_RECOVERY.md, a new root-level file in a repo whose root holds exactly one README and which nothing links to. The Test plan's three items are all unchecked, which is an accurate account of the gap below.
Blocking
-
testdata/execution_recovery_v1/completed_with_tx.json:3-4- the envelope matches no type in this repo, and Go will not tell you. The fixtures nest under"execution"with"id";cmd/execute/status.go:16-26is flat withexecutionIdand no wrapper.json.Unmarshal(fixture, &ExecStatusResponse{})therefore returns a nil error and a fully zero struct -Status == ""for all sevenexecution-shaped fixtures. A table test keyed onStatussilently takes the empty-string branch for every one of them and still passes. -> record the fixtures in the flat wire shape withhttpStatusas a sidecar, or add the envelope type and loader in this PR. -
testdata/execution_recovery_v1/reverted.json:5,13- the fixture for the failure this pack exists to prevent escapes every rule in it. It is"status": "completed"with"verified": trueand"receiptStatus": "reverted". R2 (contract.md:21) fires only when "no transaction hash / verified receipt is present" - one is present and verified. R4 (:29) listsrevertedas a status, and this fixture's status iscompleted. No rule readsreceiptStatus, so a client implementing R1-R6 literally returns success for a reverted on-chain transaction.contract.md:48maps this fixture to R4, which cannot see it. The Definitions line at:10says "verified successful receipt", but Definitions are not phrased as operative rules. -> promotereceiptStatus == "success"into R2's rule text. -
docs/execution-recovery-v1/contract.md:25,29,37- three of the six MUSTs are contradicted by the client shipped in this repo, and nothing in the document says so. R3 requires a stable idempotency key across write retries:internal/http/client.go:50-60setsRetryMax = 3and excludes only 429, so a 502 onkh ex transferreplays the buffered POST up to three times, and no idempotency key exists anywhere in the repo - up to four transfers for one user intent, which is the single most dangerous case in the pack and the one with no fixture at all. R6 says clients SHOULD ride out a first-readnot_found:cmd/execute/transfer.go:195-196returns an API error on any non-200, so--waitaborts on the cold-start 404 the pack ships a fixture for. R4 declaresrevertedandnot_foundterminal;transfer.go:30-33recognises onlycompletedandfailed, andcmd/run/status.gouses a third vocabulary (success/error/cancelled) that appears in no rule. -> add a conformance column to the fixture table, or mark each unimplemented rule as normative-but-not-yet-met.
Mechanical - actionable as-is
testdata/execution_recovery_v1/cold_start.json:2,5-httpStatus: 200with"status": "queued"is decision-identical toqueued.json, so it exercises R1, not R6. R6 is about a first read that returnsnot_found; onlynot_found.jsoncan exercise it, andcontract.md:49already maps it there. Make this a two-response sequence (404 then 200) or drop it.testdata/execution_recovery_v1/malformed.json- valid JSON, so it cannot exercise R4's "unparseable/malformed bodies" clause. It unmarshals cleanly toStatus == "", which appears in no rule's status list, so a client polls a dead execution forever. Split R4 into terminal-failure statuses and unrecognised-schema, and ship a genuinely non-JSON file if that path is meant to be covered.README_EXECUTION_RECOVERY.md:1- carries a UTF-8 BOM (ef bb bfbefore the#). No other file in the repo has one, andcontract.mdcorrectly does not.docs/execution-recovery-v1/contract.md- published nowhere, so the audience it names at:4cannot read it..github/workflows/sync-cli-docs.yml:59-60copies onlykh*.mdand:73hand-listsquickstart.md concepts.md. It also breaks nothing:docs/generate.go:47globskh*.md, sodocs-checkstays green.
Needs a decision
- Should the pack land without a consumer? - (a) merge as fixtures plus prose, costs nothing now, but
go test ./...never walkstestdata/so the fixtures and the contract can drift apart indefinitely with green CI; (b) require the loader and table test in this PR, costs more work now and is what would have caught the two blocking items above and both fixture items. - Where should a hand-written normative document live? - (a)
docs/, which is generated from the cobra tree with two hand-written exceptions enumerated by name in the sync workflow, so a third needs adding to that list; (b) outsidedocs/, stated as repo-internal, costing the doc its published home.
Verdict
Changes requested - the fixtures decode to an empty struct against this repo's own type, and the one fixture representing a reverted transaction is classified as success by the rules shipped beside it.
Adds golden status envelopes and recovery rules for KeeperHub#53. Does not implement --require-verified (see KeeperHub#95).
…cold-start Address review on PR KeeperHub#97: flat wire-shape fixtures consumed by table tests, fail closed on reverted receipts, stable Idempotency-Key on write retries, bounded not_found polling during --wait, docs sync for execution-recovery.
6ae2087 to
3505b09
Compare
mohamedwael201193
left a comment
There was a problem hiding this comment.
Thanks for the thorough review — addressed in 3505b09.
Wire shape + consumer (blocking)
- Fixtures are now the flat direct-execution shape (
executionId, not nestedexecution.id) withhttpStatusas a sidecar. - Added
internal/execrecoveryloader + table tests sogo test ./internal/execrecovery/...actually consumes every fixture. Empty-struct silent pass is no longer possible.
Reverted receipts (blocking)
- R2 now treats
receiptStatus=revertedas Failure even whenstatus=completedandverified=true. --waitfails closed on reverted receipts (cmd/execute/transfer.go+ tests).
R3 idempotency
kh ex transfer/kh ex ccsetIdempotency-Keyonce beforeDo, so go-retryablehttp retries reuse the same key.--idempotency-keypins a stable key across process restarts.- Covered by
TestTransferCmd_IdempotencyKeyStableAcrossHTTPRetries.
R6 cold start
--wait/--watchtolerate an initial HTTP 404/not_founduntil the wait deadline.cold_start.sequence.jsonis a real 404 → pending → completed+success sequence (oldcold_start.jsonremoved).
Malformed / vocabulary / docs
malformed.jsonuses a genuine non-JSONresponseRaw.- Direct-execution vs workflow-run vocabularies documented and tested separately.
- Removed BOM root
README_EXECUTION_RECOVERY.md; published hand-writtendocs/execution-recovery.mdand registered it insync-cli-docs.yml.
Note on CI check-issue-link
Title now references #53. That check still requires the accepted label on the issue — we cannot apply that label ourselves. Happy to wait on maintainer acceptance of #53 (or no-issue-required if preferred).
Happy to adjust further if anything still misses the mark.
suisuss
left a comment
There was a problem hiding this comment.
What this changes
Net diff against current main: 26 files, +1036/-40. internal/execrecovery is a new package (classify.go, fixture.go, idempotency.go, vocabulary.go + two test files) implementing and testing the R1-R6 execution-recovery contract from docs/execution-recovery-v1/contract.md. cmd/execute/transfer.go and cmd/execute/status.go gain cold-start-tolerant polling and reverted-receipt failure handling; transfer.go and contract_call.go gain a --idempotency-key flag wired to set Idempotency-Key once before client.Do, so go-retryablehttp's automatic retries reuse it. Ten JSON fixtures plus one multi-step sequence fixture live under testdata/execution_recovery_v1/. docs/execution-recovery.md is new and is now wired into the doc-sync pipeline (docs/generate.go, .github/workflows/sync-cli-docs.yml, +1/-1) so it actually reaches docs.keeperhub.com instead of sitting unpublished.
I built and ran the full suite locally against pull/97/head (Go 1.25, matching go.mod): go build ./... and go vet ./... are clean, go test ./... is green across every package, including all of this PR's new tests.
Does it match the description
Close, with two small gaps. The Test Plan checklist in the PR body still shows three unchecked boxes ("port/load fixtures in Go tests," "assert decision table," "confirm fixture IDs synthetic") - all three are now actually done; the checkboxes are just stale. Separately, the summary doesn't mention the --idempotency-key flag added to two commands, the cold-start-polling behavior change, or the one-line sync-cli-docs.yml addition that makes the doc publishable - all real changes worth a line in the description, even though none of them are risky.
Blocking
None. Every blocking item from the previous review is resolved and independently re-verified against the current head:
- The flat-vs-nested envelope mismatch that silently zero-decoded seven fixtures is fixed - fixtures now use
executionId/statusflat shape, andTestFixtures_DecodeIntoDirectStatusfails the build if any 200-status fixture decodes to an emptyStatus. reverted.jsonis no longer classified as success -classify.go'shasRevertedReceiptcheck runs unconditionally, before the success path, regardless ofRequireChainEvidence. Covered byTestRevertedIsNeverSuccessand, end to end,TestTransferCmd_WaitFailsOnRevertedReceipt(which I ran against a real httptest server).- R3 (idempotency), R4 (terminal/malformed vocab), and R6 (cold-start 404 tolerance) are no longer contradicted by the shipped client - the CLI's own
transfer.go/status.go/contract_call.gonow implement all three, each with a passing test.
Mechanical - actionable as-is
internal/execrecovery/fixture.go'sFixturestruct tags are misaligned (extra padding beforestring/bool/etc.) -gofmt -lflags it. Trivial:gofmt -w internal/execrecovery/fixture.go. (Most of the repo isn't gofmt-clean either, so this is cosmetic, not a blocker - but it's this PR's own new file.)contract_call.go's idempotency wiring is a byte-for-byte mirror oftransfer.go's, but onlytransfer.gohas a test proving the key survives an HTTP retry (TestTransferCmd_IdempotencyKeyStableAcrossHTTPRetries). Given the mechanism is identical, this is low risk, but a one-line equivalent test forcontract_call.gowould close the gap cheaply.- Update the PR description's Test Plan checkboxes to reflect that all three items are done.
With the team
#53 asks for public fixtures covering the workflow-webhook submission response (POST /api/workflows/<id>/webhook - the envelope third-party adapter authors parse after submitting a signed receipt). This pack fixtures a different, real endpoint: GET /api/execute/{id}/status, the CLI's own direct-execution polling surface. That's now confirmed at the code level too - vocabulary.go explicitly separates DirectExecutionVocabulary() (/api/execute/{id}/status) from WorkflowRunVocabulary() (/api/workflows/executions/{id}/status), and a test (TestVocabularySurfacesAreDistinct) enforces the two never share a terminal-status string. Neither vocabulary is the webhook-submission envelope #53 describes.
This pack is genuinely good, tested, now-published work on its own terms - a real gap (the CLI's polling/retry/reverted-receipt behavior had no fixture coverage or tests at all) closed thoroughly. It just isn't what #53 asked for. I'm taking the question of whether #53 should stay open with this PR referenced as related-but-not-resolving, or whether a new issue should be filed specifically for the webhook-envelope fixtures, to the team - I'd lean toward the latter, since this PR shouldn't be blocked or rescoped to also cover the webhook envelope, a materially different response shape with a different consumer (external adapter authors, not the CLI itself). I'll follow up on #53 once that's settled.
Verdict
Approve. Every prior blocking and mechanical finding is fixed and independently verified (build, vet, and full test suite pass against the actual PR head, not just by reading the diff). The remaining item - the endpoint mismatch with #53 - isn't a defect in this PR; it's a scoping question I'm taking to the team separately, and it shouldn't hold up merging good, tested, standalone work.
Dismissing - this review shouldn't have been a formal GitHub approval. The findings and verdict stand as a plain comment instead; this label means ready for a senior maintainer's final review, not a merge-ready GitHub approval.
gofmt Fixture struct tags. Prove kh ex cc reuses Idempotency-Key across HTTP retries, matching transfer.go.
|
Thanks for the detailed review and for confirming the execution-recovery pack is sound on its own terms. We've kept PR #97 scoped to the CLI execution-status recovery surface (
This is a new head, so it needs a fresh look — we are not treating the previous verdict as still live. We agree that #53 is a separate workflow-webhook contract/ownership question. The production handler lives in We would not couple those fixtures to the CLI. If the team wants a canonical public pack, it belongs next to that handler (CI in the owning service), derived from the route and its tests — not from guessed adapter shapes. The issue's 201 / 422 / 503 fixture names do not match this handler (success is 200; payload validation is 400; catch is 500). Happy to contribute that pack in |
suisuss
left a comment
There was a problem hiding this comment.
What this changes
internal/execrecovery is a new package classifying one status observation into pending / success / failure / malformed / rate_limited, driven by ten JSON fixtures under testdata/execution_recovery_v1/. Alongside it, four production changes: --idempotency-key on kh ex transfer and kh ex cc, which set an Idempotency-Key header on every POST whether or not the flag is passed (transfer.go:85-95, contract_call.go:93-103); 404 tolerance in the --wait poll loop (transfer.go:169-185) and in the --watch loop (status.go:143-147); receipt rows plus a new execOutcomeError that fails a run when a receipt reports reverted (transfer.go:252-266, status.go:125); and a new public guide.
The workflow change is .github/workflows/sync-cli-docs.yml:73, which adds execution-recovery.md to the hand-written-guide list. That workflow runs on workflow_dispatch and release: published, checks out KeeperHub/keeperhub with secrets.KEEPERHUB_PAT, wraps each named guide in Nextra frontmatter into docs/cli/, and opens a PR against staging. So that one line makes docs/execution-recovery.md a customer-facing page on docs.keeperhub.com at the next release. It grants no new permissions and touches no gate.
Since the last review the diff is one commit, d6e72e2, touching two files.
Previously raised
- gofmt on
internal/execrecovery/fixture.go: addressed, struct tags aligned at:11-20. - A contract-call test mirroring the transfer retry test: addressed,
cmd/execute/contract_call_test.go:308-364. - Stale Test Plan checkboxes: addressed.
- Description missing
--idempotency-key, the cold-start change, and thesync-cli-docs.ymlline: addressed, all three are in the body.
Four corrections on my side, because they bear on how far my earlier reviews here can be trusted:
- You were right about #53, and I was wrong to leave it hanging over this PR as an open scoping question. Every structural claim in your 2026-08-13 comment checks out: the webhook handler, its schema and its integration test all live in
KeeperHub/keeperhub, the success envelope is 200{executionId, status:"running"}, there is no fixtures repo, this CLI has no call path to the webhook, and #53's proposed 201/422/503 fixture names match none of the codes that route returns. Those fixtures belong next to the handler. I am removingdecision-needed. - My first review's blocker - that the CLI recognises only
completedandfailedwhile R4 declaresrevertedandnot_foundterminal - was wrong in the other direction. Those are receipt-level and transport-level values, not execution statuses; the CLI's two-status set matches the server enum. The contract's vocabulary needed correcting, not the client. - My second review granted "R3, R4 and R6 are no longer contradicted by the shipped client" without checking the handler. R3 is half-implemented and R6 overshoots in
--watch, both below. - The
approveon this PR was mine and I gave it too early.
The idempotency header itself is the right call and I am not asking for it to come out - before it, the same 502 replay produced a second real execution. Only the response it unlocks is unhandled.
Does it match the description
Undersells. The body names every file, but test: is the wrong type for a diff adding two user-facing flags and changing the control flow of two fund-moving commands. release-please-config.json carries no changelog-sections override, so test: gets no changelog entry and no version bump, and --idempotency-key would ship invisible. One bullet also claims more than the code does: "fail closed without chain evidence" is listed as implemented, but nothing in the CLI ever sets RequireChainEvidence, so classify.go:124-133 is reachable only from fixtures.
Split test: not one unit. The fixture pack plus internal/execrecovery is correct with everything else reverted; the idempotency header is correct with the 404 tolerance reverted; the 404 tolerance is correct with the receipt handling reverted; the docs page plus the sync line is correct with all the Go changes reverted. Four shippable sides.
Blocking
-
cmd/execute/transfer.go:104,cmd/execute/contract_call.go:113- sending the header unconditionally makes a 409 reachable that the CLI could never receive before, and both sites classify on the status code alone ->lib/idempotency.tson the app repo'sstagingreturns 409 for bothidempotency_conflict(retryable:false) andidempotency_in_progress(retryable:true), separated only bycode.internal/http/client.gosetsRetryMax=3, so a 504 during a slow on-chain wait replays the POST, the replay carries the same key, hits the in-flight lock and returns 409;transfer.go:104surfaces it as an error, the user re-runs,ResolveIdempotencyKey("")mints a fresh key, and the transfer broadcasts twice. Fix: branch on the body'scodeat both sites - retry the same key onidempotency_in_progress, report a payload mismatch onidempotency_conflict- or gate the header behind an explicit--idempotency-keyuntil that handling lands. -
cmd/execute/status.go:143-147- the 404continueruns inside a loop with no deadline.watchExecStatusonmainterminates only on a terminal status or an error, and this converts the one error that reliably terminated it into a retry ->kh ex st <id> --watchagainst a mistyped id exits immediately today; at head it spins forever, and under--jsonor a non-TTY it spins silently with no output. This is reachable with a well-formed id:app/api/execute/[executionId]/status/route.tsfilters onorganizationId, so another org's execution also answers 404. Fix: give--watcha deadline and scope 404-as-pending to it, or leavewatchExecStatusalone and keep the tolerance inpollExecStatus, which already has one attransfer.go:159. -
docs/execution-recovery.md:16anddocs/execution-recovery-v1/contract.md:33- the rule this pack is built around describes a response the API cannot produce, andsync-cli-docs.yml:73publishes it ->completeExecutioninapp/api/execute/_lib/execution-service.tsre-verifies every claimed hash before writingcompleted;allVerifiedisevery(r => r.verified)inlib/web3/verify-receipt.ts, a reverted receipt is writtenverified:false, and that file's header states a hash that cannot be positively confirmed "resolves toverified: false, neververified: true". Reverted is conclusive, so the row settles asfailed.{status:"completed", verified:true, receiptStatus:"reverted"}is precisely the state the KEEP-966 gate exists to make unreachable, yet it is whatreverted.jsonpresents as a wire sample and what the guide tells integrators to expect.contract.md:18compounds it by listingqueuedas a direct-execution status when the enum inapp/api/execute/_lib/types.tsispending|running|unconfirmed|completed|failed. Fix: restate the receipt rule as a client-side invariant ("do not infer success fromstatusalone"), dropqueued, markreverted.jsonas a defensive fixture rather than an observed envelope - or drop thesync-cli-docs.ymlline from this PR and publish once the wording is checked against the handler.
Mechanical - actionable as-is
internal/execrecovery/classify.go:141,cmd/execute/transfer.go:261- only one of four non-success receipt states is treated as failure.lib/db/schema-extensions.tsdefinessuccess | reverted | not_found | timeout | safe_inner_failure; asafe_inner_failurereceipt returns nil fromexecOutcomeError, so--waitexits 0 on a Safe whose inner call failed. MatchreceiptStatus != "success", or enumerate all five.internal/execrecovery/classify.go:102-110- the switch acceptsqueued,not_found,error,cancelledandsuccess, none of which the endpoint emits, and three of which are the workflow vocabulary the doc comment at:60-62forbids feeding here whileTestVocabularySurfacesAreDistinctasserts the two never overlap. Meanwhiledefault:returns malformed, so a status the server adds later reads as a corrupt body.cmd/execute/transfer.go:179-statusResp.Status == "not_found"is unreachable; the endpoint answers 404 with{"error":"Execution not found"}and nostatusfield.cmd/execute/transfer.go:219- dead branch; theStatusNotFoundcheck returns exactly what the!= StatusOKcheck on the next line already returns.internal/execrecovery/fixture_test.go:63-TestFixtures_ClassifyTablehas nolen(fixtures)guard, so it passes on an empty load. Renaming any fixture to*.sequence.jsonmakesfixture.go:50-55skip it silently and the table still goes green. Add the guard used at:27, or assert an expected count.internal/execrecovery/fixture.go:11-20- nothing carries or checks a version. The directory says_v1butFixturehas no version field andjson.Unmarshalignores unknown keys, so a v2 fixture dropped here is graded by v1 rules silently. Require aversionand fail the load when it is not 1.- Fixture fidelity against the real envelope:
code:"not_found"innot_found.jsonandcold_start.sequence.jsonis never sent;verifiedAtis required onDirectExecutionReceiptEntryand every fixture omits it;chainIdis optional server-side and every fixture sets it; andqueued.jsonis named for a status its body does not contain ("status":"pending"), so thequeuedbranch has no coverage. internal/execrecovery/classify.go:135andcontract.md:34reference--require-verified, which does not exist in this repo.classify.go:31,39duplicateExecStatusResponse/ExecReceiptfromstatus.go:18-43field for field, which is why the exportedHasRevertedReceiptatclassify.go:158cannot be called fromcmd/executeandexecOutcomeErrorre-implements it. One definition should go.- Retitle to
feat:so the two new flags reach the changelog.
With the team
- Does the receipt-classification layer earn its place now that the server gate makes
completedplus a reverted receipt unreachable? Keeping it is fail-closed defence in depth and costs a second copy of the wire types plus a rule set that drifts from the handler with no CI signal in this repo; dropping it relies onstatus, whichexecOutcomeErroralready reads correctly, and loses the guard if the gate regresses. I lean toward keeping it and relabelling it a client-side invariant, but I want the API owners on it. I am discussing this with the core team now and will come back with a verdict shortly. - Should
--waitexit non-zero when it times out onunconfirmed? The reconciler is still watching by design, and the server's own comment warns that calling such a write failed is what invites the retry that broadcasts a second transaction. Exiting non-zero keeps scripts honest about not having a confirmed result but risks that double send; exiting zero with an explicit unconfirmed status is safer for funds and weaker as a gate. Unchanged by this PR, but this pack is the first thing to nameunconfirmedas first-class. I am discussing this with the core team now and will come back with a verdict shortly.
Verdict
Changes requested, on three items introduced by the production half of this diff: an unhandled 409 on the write path, an unbounded --watch loop, and a customer-facing docs page stating behaviour the handler contradicts. The fixture pack and loader are not what I am holding, and this increment closed both items I left open.
|
This also clears the trap in my review: retitling to |
Handle 409 idempotency_in_progress vs conflict by body code, bound --watch 404, and align receipts/docs with the server enum and KEEP-966.
|
Thanks for the corrections — especially the 409 split, the unbounded This head addresses the three blockers and the mechanical items against the current server ( 409.
Docs / fixtures. Direct-execution enum is Title is now
Please re-review this head. |
An unreadable receipt is not a failed transaction. The wait paths treated `unconfirmed` as pending, so `--wait` and `--watch` polled it to an expired budget and exited non-zero. That non-zero exit invites a re-run of an intent whose transaction may already be on chain, which is the double-broadcast the idempotency work in this branch exists to prevent. Stop on `unconfirmed` and report it instead. `--wait` exits zero there, printing the status and transaction hash; the server keeps reconciling the row and the settled status can be read later against the same execution ID. Classify gains a distinct `unconfirmed` outcome so the classifier cannot say "pending" while the CLI stops, and the direct-execution vocabulary lists it under the client stop-waiting set. Pending/Terminal are documented as client wait semantics, not a claim that the server considers the row final: the server's own type doc keeps `unconfirmed` non-terminal.
|
You closed all three blockers before I got back to this, and I checked each one rather than taking it on trust. The 409 handling in Every mechanical item was done too. I implemented none of them. What was missing was the One framing point I want on the record, because it bears on the page CI publishes. The server documents The contract bump to 1.3.0 is right - R1 changed normatively. |
Keep approved PR KeeperHub#97 idempotency and wait/watch semantics while taking main's --require-verified status gating and KEEP-1191 failed-write exit.
Keep approved PR KeeperHub#97 idempotency, watch-404, and receipt outcomes while taking main's X-Poll-Interval-Hint loop and completed-without-hash reconciliation.
|
Hey @mohamedwael201193. I'll drive this one home now. Well done, thanks for your help |
SA9003: the rotate/new-key check had no body; the following assertion already requires do not retry with a new key.
Both pages said the CLI does not implement --require-verified. KeeperHub#95 landed it on kh ex status, so the claim was false and contradicted kh_execute_status.md in this same branch. State what the flag does instead: without it a completed execution with an empty receipts array is still success, with it the CLI exits non-zero unless every receipt is verified with receiptStatus success, and unconfirmed fails the gate. Contract bumped to 1.4.0 - sync-cli-docs publishes this page on release.
|
You got to the merge before I did, and your resolution is the right one - I checked it rather than assuming. The hint-driven loop from #99 survived with I pushed one commit on top, for something the merge could not have told you about. Both They now say what the flag actually does, read off Your framing of Pending and Terminal as client wait semantics is untouched - that distinction is the reason this page can describe stopping on Build, vet and |
Summary
CLI execution-status recovery for
GET /api/execute/{id}/statusand the matchingkh ex transfer/kh ex ccwrite paths.This PR does not implement
POST /api/workflows/<id>/webhookand does not resolve Issue #53.What shipped
version: 1) JSON fixtures undertestdata/execution_recovery_v1/, labeledobserved/defensive/classifier. Loader fails on missing version. Tests assert fixture count and sequence count.pending | running | unconfirmed | completed | failed. Unknown future statuses classify asunrecognized(never success, never malformed).--idempotency-keyonkh ex transferandkh ex cc. HTTP 5xx retries reuse the same key.codefromlib/idempotency.ts:idempotency_in_progress→ retry the same key until--timeoutidempotency_conflict→ fail; do not mint a new key--waittolerates a bounded initial HTTP 404 until--timeout. Persistent 404 is a timeout error.--watchtreats 404 as a terminal error (mistyped id / other org). It does not loop.receiptStatus=successis the only successful receipt.revertedandsafe_inner_failurefail the wait path.not_found/timeoutonunconfirmedstay non-terminal (server KEEP-966). A defensivecompleted+ non-success receipt also fails; that combination is not an observed production envelope.docs/execution-recovery.mdis registered insync-cli-docs.yml. Wording matches the handler (noqueued, no fake--require-verified, reverted fixture labeled defensive).Canonical wire type:
execrecovery.DirectStatus(aliased asExecStatusResponseincmd/execute).Non-goals
--require-verified.RequireChainEvidenceon production wait paths (classifier-only fixture option).Test plan
go test ./internal/execrecovery/...(version: 1, expected counts, sequence included)TestFixtures_ClassifyTable, receipt-state tests)exec_fixture_*); kinds labeled observed/defensive/classifier--idempotency-keyon transfer and contract-call; 502/504 retries keep the same keyidempotency_in_progressretries the same key; 409idempotency_conflictfails without a new key--waitcold-start 404 then 200; persistent 404 times out--watch404 (including--json/ non-TTY) exits instead of loopingsafe_inner_failurereceipts fail waitdocs/execution-recovery.mdaligned withapp/api/execute/_lib/types.tsand KEEP-966go test ./internal/execrecovery/...andgo test ./cmd/execute/...go build ./...andgo vet ./...